{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center> Приемы и подводные камни при обработке текстов на примере sentiment analisys. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Рассмотрим как влияют различные преобразования текстов и подбор параметров CountVectorizer на результаты sentiment analysis(анализ настроения) на примере применения логистической регрессии. \n",
    "\n",
    "Напомню, что эта задача является крайне актуальной везде, где необходимо понять, является ли данный отзыв положительным или отрицательным. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Возьмем тренировочный датасет labeledTrainData.tsv из вот этого [соревнования Kaggle](https://www.kaggle.com/c/word2vec-nlp-tutorial/data). Там же можно ознакомиться с подробным описанием их содержимого. \n",
    "\n",
    "Для анализа данных текстов, представим их в виде векторов, посчитав частотность слов с помощью CountVectorizer). Сфомируем на основе нашего тренировочного датасета \"labeledTrainData.tsv\" объект Pandas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from bs4 import BeautifulSoup\n",
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#import logging\n",
    "#import time\n",
    "\n",
    "\n",
    "\n",
    "%matplotlib inline\n",
    "from matplotlib import pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train = pd.read_csv(\"labeledTrainData.tsv/labeledTrainData.tsv\", header=0, \\\n",
    "                    delimiter=\"\\t\", quoting=3)\n",
    "#http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на структуру данного датасета:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train.head(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный датасет содержит 25000 строк. Для экономии времени и ресурсов,попробуем его уменьшить, не ухудшая качетсво (пропорции между количеством положительных и отрицательных отзывов)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Определим списки индексов положительных и отрицательных отзывов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "idx_pos=[]#индексы позитивных текстов\n",
    "idx_neg=[]#индексы негативных текстов\n",
    "for i in range(train.shape[0]):\n",
    "    if train[\"sentiment\"][i]==1:\n",
    "        idx_pos.append(i)\n",
    "    else:\n",
    "        idx_neg.append(i)        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "len(idx_pos)#ровно половина"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы видим, что у нас равное количество положительных и отрицательных отзывов.  возьмем по 3000 положительных и отрицательных отзывов из train. Все результаты преобразований будем проверять на логистической регрессии."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отберем по 3000 положительных и отрицательных отзывов и запишем их по этом порядке в общий список train_reviews"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_reviews = 3000\n",
    "train_reviews = []\n",
    "k=0\n",
    "for i in (idx_pos):\n",
    "    if k<num_reviews:\n",
    "        train_reviews.append( train[\"review\"][i] )# list of processed pos reviews\n",
    "        k+=1\n",
    "    else: break     \n",
    "k=0\n",
    "for i in (idx_neg):\n",
    "    if k<num_reviews:\n",
    "        train_reviews.append(train[\"review\"][i] )# list of processed neg reviews\n",
    "        k+=1\n",
    "    else: break      "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В этом случае мы можем составить список значений целевой переменной на это выборке как: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sentiments = [1]*num_reviews+[0]*num_reviews"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### №1 Не делаем никаких специальных преобразований - получаем baseline виде roc_auc_score = 0.9331393769356535"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_auc_lr_valid(X, y, C=1.0, seed=17,test_size=0.3):\n",
    "    X_train,y_train  = X,y\n",
    "    X_train_part, X_valid, y_train_part, y_valid = train_test_split(X_train, y_train, test_size=0.3, random_state=17)\n",
    "    logit = LogisticRegression(C=C, random_state=seed)\n",
    "    logit.fit(X_train_part, y_train_part)\n",
    "    logit_valid_pred = logit.predict_proba(X_valid)[:, 1]\n",
    "    score = roc_auc_score(y_valid, logit_valid_pred)\n",
    "    \n",
    "    return score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer1 = CountVectorizer(analyzer = \"word\") \n",
    "train_data_features1 = vectorizer1.fit_transform(train_reviews)\n",
    "train_data_features1 = train_data_features1.toarray()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы получили матрицу признаков размером 42249"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.DataFrame(train_data_features1).shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "score = get_auc_lr_valid(train_data_features1, sentiments)\n",
    "score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### №2 Добавим параметр n_gram_range (и ограничим сразу число max_features, чтобы не вылетать по памяти).\n",
    "Ограничение, кстати, надо делать очень аккуратно, т.к. при введении этой константы,  результат может как улучшиться, так и  ухудшиться, по сравнению с базовым. Посмотрим,что получится при применении разных таких ограничений. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm import tqdm_notebook"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#посмотрим, как поведет себя результат для различных значений max_features\n",
    "scores=[]\n",
    "max_f=range(5000,45000,5000)\n",
    "\n",
    "for max_features in tqdm_notebook(max_f):\n",
    "    vectorizer2 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=max_features) \n",
    "    train_data_features2 = vectorizer2.fit_transform(train_reviews)\n",
    "    scores.append(get_auc_lr_valid(train_data_features2.toarray(), sentiments))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import gc\n",
    "\n",
    "gc.collect()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot(max_f, scores, 'ro-')\n",
    "plt.xscale  ('linear')\n",
    "plt.xlabel('max_features')\n",
    "plt.ylabel('AUC-ROC')\n",
    "plt.title('Подбор max_features')\n",
    "# горизонтальная линия -- качество модели с коэффициентом по умолчанию\n",
    "plt.axhline(y=score, linewidth=.5, color = 'b', linestyle='dashed') \n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Таким образом, мы видим, что если взять max_features =5000, результат получится хуже,чем до введения n_grams.\n",
    "\n",
    "#### Возьмем за следующий baseline максимальный полученный результат, roc_auc_score =0.9429874389358412 и зафиксируем max_features =40000 "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### №3 Уберем все символы, кроме латинских букв, передав функцию токенизации в CountVectorizer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def tokenizer1( raw_review ):\n",
    "     # 1. Убираем HTML\n",
    "    review_text = BeautifulSoup(raw_review,'lxml').get_text() \n",
    "    \n",
    "    # 2. Убираем все символы, не являющиеся буквами. \n",
    "    letters_only = re.sub(\"[^a-zA-Z]\", \" \", review_text) \n",
    "    \n",
    "    # 3. Cоздадим список слов\n",
    "    words = letters_only.split()        \n",
    "    \n",
    "    meaningful_words = [w for w in words]  \n",
    "    return meaningful_words\n",
    "     "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer3_1 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=40000,tokenizer=tokenizer1) \n",
    "train_data_features3 = vectorizer3_1.fit_transform(train_reviews)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_auc_lr_valid(train_data_features3.toarray(), sentiments)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Результат улучшился и составил roc_auc_score = 0.94340482388331"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь сделаем такую же очистку вручную, подав Count_Vectorizer на вход уже очищенные данные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def review_to_string( raw_review ):\n",
    "    # 1. Убираем HTML\n",
    "    review_text = BeautifulSoup(raw_review,'lxml').get_text() \n",
    "    \n",
    "    # 2. Убираем все символы, не являющиеся буквами. \n",
    "    letters_only = re.sub(\"[^a-zA-Z]\", \" \", review_text) \n",
    "    \n",
    "    # 3. Cоздадим список слов\n",
    "    words = letters_only.split()                             \n",
    "    \n",
    "    meaningful_words = [w for w in words]   \n",
    "    \n",
    "    # 4. Переведем список в строку, состоящую из слов, разделенных пробелом\n",
    "    return( \" \".join( meaningful_words ))   "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Но для этого придется снова формировать наш train set - train_reviews_clean"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_reviews = 3000\n",
    "train_reviews_clean = []\n",
    "k=0\n",
    "for i in (idx_pos):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean.append(review_to_string( train[\"review\"][i]) )# list of processed pos reviews\n",
    "        k+=1\n",
    "    else: break     \n",
    "k=0\n",
    "for i in (idx_neg):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean.append(review_to_string(train[\"review\"][i]))# list of processed neg reviews\n",
    "        k+=1\n",
    "    else: break      "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer3_2 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=40000) \n",
    "train_data_features3 = vectorizer3_2.fit_transform(train_reviews_clean)\n",
    "get_auc_lr_valid(train_data_features3.toarray(), sentiments)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Результат еще улучшился! \n",
    "Получается, что очистка данных на входе работает лучше, чем передача такой функции в tokenizer. \n",
    "Наш новый baseline 0.9437382378946015"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### №4 Добавим стандартные stop_words"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import nltk\n",
    "from nltk.corpus import stopwords\n",
    "\n",
    "stops=stopwords.words(\"english\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer4_1 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=40000,stop_words=stops) \n",
    "train_data_features4 = vectorizer4_1.fit_transform(train_reviews_clean)\n",
    "get_auc_lr_valid(train_data_features4.toarray(), sentiments)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Вопреки ожиданиям, результат ухудшился!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Почистим стоп-слова вручную, предварительно переведя весь текст в lower_case. Также уберем слова длиной менее 2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def review_to_string2( raw_review ):\n",
    "    # 1. Убираем HTML\n",
    "    review_text = BeautifulSoup(raw_review,'lxml').get_text() \n",
    "    \n",
    "    # 2. Убираем все символы, не являющиеся буквами. \n",
    "    letters_only = re.sub(\"[^a-zA-Z]\", \" \", review_text) \n",
    "    \n",
    "    # 3. Переведем в нижний регистр и создадим список слов\n",
    "    words = letters_only.lower().split()                             \n",
    "    #\n",
    "    # 4. Для ускорения работы в Питоне, переведем стоп-слова из формата list в формат set.  \n",
    "    stops = set(stopwords.words(\"english\"))                  \n",
    "    # \n",
    "    # 5. Уберем все стоп-слова (т.е. слова,слова, знаки, символы, которые самостоятельно не несут никакой смысловой нагрузки)\n",
    "    meaningful_words = [w for w in words if (not w in stops) and len(w)>2]   \n",
    "    #\n",
    "    # 6. Переведем список в строку, состоящую из слов, разделенных пробелом\n",
    "    return( \" \".join( meaningful_words )) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Снова применим данное преобразование к начальному датасету:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_reviews = 3000\n",
    "train_reviews_clean2 = []\n",
    "k=0\n",
    "for i in (idx_pos):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean2.append(review_to_string2( train[\"review\"][i]) )# list of processed pos reviews\n",
    "        k+=1\n",
    "    else: break     \n",
    "k=0\n",
    "for i in (idx_neg):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean2.append(review_to_string2(train[\"review\"][i]))# list of processed neg reviews\n",
    "        k+=1\n",
    "    else: break  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer4_2 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=40000) \n",
    "train_data_features4 = vectorizer4_2.fit_transform(train_reviews_clean2)\n",
    "get_auc_lr_valid(train_data_features4.toarray(), sentiments)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Результат ручной чистки стоп-слов оказался лучше, чем автоматической, но, вопреки ожиданиям, убирание стоп-слов ухудшило результат!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### №5 Попробуем применить стемминг.\n",
    "Добавим ее в нашу функцию преобразования данных. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.stem.snowball import SnowballStemmer\n",
    "\n",
    "stemmer = SnowballStemmer(\"english\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def review_to_string3( raw_review ):\n",
    "    # 1. Убираем HTML\n",
    "    review_text = BeautifulSoup(raw_review,'lxml').get_text() \n",
    "    \n",
    "    # 2. Убираем все символы, не являющиеся буквами. \n",
    "    letters_only = re.sub(\"[^a-zA-Z]\", \" \", review_text) \n",
    "    \n",
    "    # 3. Переведем в нижний регистр и создадим список слов\n",
    "    words = letters_only.lower().split()  \n",
    "    \n",
    "            \n",
    "    # 4. Применим стемминг\n",
    "    meaningful_words = [stemmer.stem(w) for w in words if len(w)>2]   \n",
    "    \n",
    "    # 5. Переведем список в строку, состоящую из слов, разделенных пробелом\n",
    "    return( \" \".join( meaningful_words )) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Снова применим данное преобразование к начальному датасету:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "num_reviews = 3000\n",
    "train_reviews_clean3 = []\n",
    "k=0\n",
    "for i in (idx_pos):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean3.append(review_to_string3( train[\"review\"][i]) )# list of processed pos reviews\n",
    "        k+=1\n",
    "    else: break     \n",
    "k=0\n",
    "for i in (idx_neg):\n",
    "    if k<num_reviews:\n",
    "        train_reviews_clean3.append(review_to_string3(train[\"review\"][i]))# list of processed neg reviews\n",
    "        k+=1\n",
    "    else: break  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorizer5 = CountVectorizer(analyzer = \"word\",ngram_range=[1,2],max_features=40000) \n",
    "train_data_features5 = vectorizer5.fit_transform(train_reviews_clean3)\n",
    "get_auc_lr_valid(train_data_features5.toarray(), sentiments)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Результат снова улучшился!\n",
    "Таким образом, с потощью стемминга нам удалось повысить roc_auc_score до 0.9442309497112883"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Таким образом, с помощью проделанных выше преобразований нам удалось повысить нашу метрику качества roc_auc_score  с базового уровня 0.933 до 0.944"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Использованные материалы:\n",
    "http://textminingonline.com/dive-into-nltk-part-iv-stemming-and-lemmatization\n",
    "http://nbviewer.jupyter.org/github/MatthieuBizien/Bag-popcorn/blob/master/Kaggle-Word2Vec.ipynbhttps://www.quora.com/What-is-the-difference-between-TfidfVectorizer-and-CountVectorizer-1\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Автор:Доможирова Наталья Николаевна (dnn37@mail.ru)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
